feat(webwalker): add configurable per-card hotkeys#1748
feat(webwalker): add configurable per-card hotkeys#1748chsami merged 2 commits intochsami:developmentfrom
Conversation
Adds 10 user-configurable hotkeys to the WebWalker side panel, each rendered inline on its category card via MicrobotHotkeyButton: 8 toggles for the selected target (custom location, bank, deposit box, slayer master, quest, clue, farming, hunter) and 2 toggles for "nearest bank" / "nearest deposit box". Keybinds are stored as hidden @configitem entries so they persist without cluttering the settings UI. The plugin wires each one through HotkeyListener; toggleCategory routes start/stop through the existing panel helpers so logs and walker state match the Start/Stop buttons. MicrobotHotkeyButton promoted from package-private to public so the WebWalker panel can embed it. Also adds a plain ':client:run' Gradle task alongside the existing 'runDebug' (no JDWP suspend). Hardcoded CTRL+X stop hotkey is intentionally preserved.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis pull request adds hotkey support to the ShortestPath plugin. The changes include: (1) registering a new Gradle Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java`:
- Around line 738-774: The async nearest-deposit-box lookup can resume walking
after the user has stopped; mirror the guard used in startWalkingNearestBank
inside startWalkingNearestDepositBox by capturing the same cancellation/token
value before calling
CompletableFuture.supplyAsync(Rs2DepositBox::getNearestDepositBox) and, inside
thenAccept, verify the captured token (and any panel/enabled state check used by
startWalkingNearestBank) still matches the current token/state before calling
startWalking(nearestDepositBox.getWorldPoint()); otherwise skip calling
startWalking to prevent restarting after Stop.
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java`:
- Around line 964-979: toggleCategory's null-guard is ineffective because
hotkeyPressed binds instance method references (e.g., panel::getBankTarget)
capturing a possibly-null panel and causing NPEs before toggleCategory runs;
change callers to pass unbound suppliers or safe lambdas that read panel at call
time (for example use ShortestPathPanel::getBankTarget or () -> {
ShortestPathPanel p = panel; return p == null ? null : p.getBankTarget(); }) so
the supplier evaluates panel null-check inside toggleCategory, and keep the
existing panel == null check in toggleCategory to short-circuit when
appropriate; update all call sites that currently pass bound references (lines
invoking toggleCategory with panel::...) to use unbound method references or the
safe lambda form.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 91c28d9e-be8f-47cf-96e7-290fe8c11de9
📒 Files selected for processing (5)
runelite-client/build.gradle.ktsrunelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathConfig.javarunelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.javarunelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.javarunelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotHotkeyButton.java
| void startWalkingNearestBank() | ||
| { | ||
| CompletableFuture.supplyAsync(Rs2Bank::getNearestBank) | ||
| .thenAccept(nearestBank -> { | ||
| if (nearestBank != null) | ||
| { | ||
| startWalking(nearestBank.getWorldPoint()); | ||
| } | ||
| else | ||
| { | ||
| Microbot.log("WebWalker: could not find a nearest bank."); | ||
| } | ||
| }) | ||
| .exceptionally(ex -> { | ||
| Microbot.log("Error while finding the nearest bank: " + ex.getMessage()); | ||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| void startWalkingNearestDepositBox() | ||
| { | ||
| CompletableFuture.supplyAsync(Rs2DepositBox::getNearestDepositBox) | ||
| .thenAccept(nearestDepositBox -> { | ||
| if (nearestDepositBox != null) | ||
| { | ||
| startWalking(nearestDepositBox.getWorldPoint()); | ||
| } | ||
| else | ||
| { | ||
| Microbot.log("WebWalker: could not find a nearest deposit box."); | ||
| } | ||
| }) | ||
| .exceptionally(ex -> { | ||
| Microbot.log("Error while finding the nearest deposit box: " + ex.getMessage()); | ||
| return null; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Nearest lookups can restart walking after Stop.
These async lookups are fire-and-forget. If the user stops walking, disables the panel, or presses the nearest hotkey a second time before the lookup completes, the old thenAccept(...) still calls startWalking(...) and walking resumes anyway.
Possible fix
+import java.util.concurrent.atomic.AtomicLong;
...
+ private final AtomicLong nearestLookupToken = new AtomicLong();
...
void stopWalking()
{
+ nearestLookupToken.incrementAndGet();
Microbot.log("Web walking stopping..");
plugin.getShortestPathScript().setTriggerWalker(null);
Rs2Walker.setTarget(null);
}
...
void startWalkingNearestBank()
{
+ long token = nearestLookupToken.incrementAndGet();
CompletableFuture.supplyAsync(Rs2Bank::getNearestBank)
.thenAccept(nearestBank -> {
+ if (token != nearestLookupToken.get())
+ {
+ return;
+ }
if (nearestBank != null)
{
startWalking(nearestBank.getWorldPoint());
}Apply the same token check to startWalkingNearestDepositBox().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPanel.java`
around lines 738 - 774, The async nearest-deposit-box lookup can resume walking
after the user has stopped; mirror the guard used in startWalkingNearestBank
inside startWalkingNearestDepositBox by capturing the same cancellation/token
value before calling
CompletableFuture.supplyAsync(Rs2DepositBox::getNearestDepositBox) and, inside
thenAccept, verify the captured token (and any panel/enabled state check used by
startWalkingNearestBank) still matches the current token/state before calling
startWalking(nearestDepositBox.getWorldPoint()); otherwise skip calling
startWalking to prevent restarting after Stop.
There was a problem hiding this comment.
This should probably be fixed. Walking should stop after pressing stop.
There was a problem hiding this comment.
@runsonmypc, agreed — the fix is straightforward. Here's the plan:
- Add
AtomicLong nearestLookupTokenfield toShortestPathPanel - Increment token in
stopWalking()to invalidate any in-flight lookups - Capture + check token in both
startWalkingNearestBank()andstartWalkingNearestDepositBox()before callingstartWalking()
🧠 Learnings used
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*Config*.java : Configure Config UI tweaks and custom controls in `MicrobotConfigPanel` rather than RuneLite's default config panel
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-10T06:02:28.166Z
Learning: Applies to runelite-client/src/**/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java : Preserve the hidden/always-on nature of `MicrobotPlugin` and its config panel wiring
Learnt from: Krulvis
Repo: chsami/Microbot PR: 1506
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java:1412-1423
Timestamp: 2025-09-11T09:51:12.404Z
Learning: In the microbot pathfinding system, global (null-keyed) transports like teleportation spells are automatically added to the player's start worldpoint by PathfinderConfig.refreshTeleports method during pathfinder refresh, eliminating the need to manually union null-keyed transports when iterating through available transports.
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-10T06:02:28.166Z
Learning: Applies to runelite-client/src/**/java/net/runelite/client/plugins/microbot/util/**/*.java : Always use the settings search bar instead of navigating tabs. Tab indices shift on game updates and the 'Interfaces' tab was removed — hardcoded tab navigation will break
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*Script.java : Check and respect `Microbot.getBlockingEventManager().shouldBlockAndProcess()` to allow high-priority events to interrupt automation
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*.java : Always use `new Rs2NpcQueryable().withName(name).nearest()` instead of deprecated `Rs2Npc.getNpc(name)` static methods
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*Script.java : Always wait for game state changes after interactions using `sleepUntil()` instead of assuming immediate completion
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-10T06:02:28.166Z
Learning: Applies to runelite-client/src/**/java/net/runelite/client/plugins/microbot/**/*.java : Never use static sleeps like `sleep(12000)` to wait for game state. Always use conditional dynamic sleeps: `sleepUntil(BooleanSupplier awaitedCondition)` (optionally with a timeout as a safety net)
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*Script.java : Integrate with `Rs2Antiban` utilities and respect `Microbot.pauseAllScripts.get()` for break handler compatibility
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md:0-0
Timestamp: 2026-04-12T01:06:30.165Z
Learning: Applies to runelite-client/src/main/java/net/runelite/client/plugins/microbot/**/*.java : Never block the client thread with `sleep()` or long-running operations; always use `Microbot.getClientThread().runOnClientThread()` for safe client access
Learnt from: CR
Repo: chsami/Microbot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-10T06:02:28.166Z
Learning: Applies to runelite-client/src/**/java/net/runelite/client/plugins/microbot/util/**/*.java : Do not hardcode cache tab indices or assume widget parent structure; always use the settings search bar and widget click by text when possible
- 📌 Commit the changes to this branch
| private void toggleCategory(String categoryName, Supplier<WorldPoint> targetSupplier) { | ||
| if (panel == null || !Microbot.isLoggedIn()) { | ||
| return; | ||
| } | ||
| WorldPoint target = targetSupplier.get(); | ||
| if (target == null) { | ||
| Microbot.log("WebWalker: no " + categoryName + " selected in the panel."); | ||
| return; | ||
| } | ||
| WorldPoint current = shortestPathScript.getTriggerWalker(); | ||
| if (target.equals(current)) { | ||
| panel.stopWalking(); | ||
| } else { | ||
| panel.startWalking(target); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | sed -n '964,979p'Repository: chsami/Microbot
Length of output: 756
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | sed -n '1007,1084p'Repository: chsami/Microbot
Length of output: 3600
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | sed -n '1,100p'Repository: chsami/Microbot
Length of output: 5196
🏁 Script executed:
grep -n "panel = null" runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.javaRepository: chsami/Microbot
Length of output: 84
🏁 Script executed:
grep -n "shutdown()" runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.javaRepository: chsami/Microbot
Length of output: 101
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | sed -n '263,275p'Repository: chsami/Microbot
Length of output: 1023
🏁 Script executed:
grep -n "panel =" runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | head -20Repository: chsami/Microbot
Length of output: 350
🏁 Script executed:
grep -n "keyManager.registerKeyListener\|new HotkeyListener" runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | head -30Repository: chsami/Microbot
Length of output: 2016
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java | sed -n '200,310p'Repository: chsami/Microbot
Length of output: 5423
🏁 Script executed:
grep -n "extends HotkeyListener\|HotkeyListener implements\|class HotkeyListener" runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.javaRepository: chsami/Microbot
Length of output: 121
🏁 Script executed:
cat -n runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java | head -50Repository: chsami/Microbot
Length of output: 2370
Remove the ineffective null-guard or pass unbound references.
The panel == null check at line 965 cannot protect bound method references like panel::getBankTarget (lines 1010, 1017, 1036, 1055, 1062, 1069, 1076, 1083): the receiver is captured when the reference is created in hotkeyPressed(), so a null panel causes an NPE before toggleCategory() runs.
While the shutdown code correctly unregisters listeners before nulling panel (lines 263–275), this pattern is fragile and creates inconsistency with other listeners that check panel == null directly (lines 1024, 1043). Passing unbound references lets the null-check actually work:
Suggested fix
-import java.util.function.Supplier;
+import java.util.function.Function;
...
- private void toggleCategory(String categoryName, Supplier<WorldPoint> targetSupplier) {
- if (panel == null || !Microbot.isLoggedIn()) {
+ private void toggleCategory(String categoryName, Function<ShortestPathPanel, WorldPoint> targetSupplier) {
+ ShortestPathPanel currentPanel = panel;
+ if (currentPanel == null || !Microbot.isLoggedIn()) {
return;
}
- WorldPoint target = targetSupplier.get();
+ WorldPoint target = targetSupplier.apply(currentPanel);Then call with unbound references: ShortestPathPanel::getBankTarget.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java`
around lines 964 - 979, toggleCategory's null-guard is ineffective because
hotkeyPressed binds instance method references (e.g., panel::getBankTarget)
capturing a possibly-null panel and causing NPEs before toggleCategory runs;
change callers to pass unbound suppliers or safe lambdas that read panel at call
time (for example use ShortestPathPanel::getBankTarget or () -> {
ShortestPathPanel p = panel; return p == null ? null : p.getBankTarget(); }) so
the supplier evaluates panel null-check inside toggleCategory, and keep the
existing panel == null check in toggleCategory to short-circuit when
appropriate; update all call sites that currently pass bound references (lines
invoking toggleCategory with panel::...) to use unbound method references or the
safe lambda form.
Bound method refs like `panel::getBankTarget` evaluate the receiver at expression time (JLS 15.13.3), so the null guard inside toggleCategory was unreachable — an NPE would fire at the method-ref expression before the check ran. Switched to unbound refs (`ShortestPathPanel::getX`) and moved dereference behind the null check via a locally-captured panel.

Summary
Test plan